package nimbus.commands
{
	import flash.events.Event;
	
	public class AsyncMacroCommand extends AsyncCommand
	{
		
		/**
		 * @protected
		 * The list of commands to be executed.
		 */
		protected var commands:Array;
		
		/**
		 * @protected
		 * An array of the commands that are incomplete.
		 */
		protected var incompleteCommands:Array;
		
		private var commandFailure:Boolean = false;
		
		/**
		 * @param commands An array of asynchronous commands to be executed.
		 */
		public function AsyncMacroCommand(commands:Array)
		{
			for each (var command:Object in commands) {
				if (!(command is AsyncCommand)) {
					throw new Error('Only asynchronous commands may be used in asynchronous ' + 
							'macro commands.');
				}				
			}
			
			this.commands = commands;
		}
		
		/**
		 * Runs through the command list in order, executing each.
		 */
		override public function execute():void
		{
			var command:AsyncCommand;
			
			// If there aren't any commands to be executed, the macro is finished.
			if (commands.length == 0)
			{
				macroComplete(false);
				return;
			}
			
			incompleteCommands = new Array();
			for each (command in commands) {
				incompleteCommands.push(command);
				AsyncCommand(command).addEventListener(AsyncCommand.EXECUTION_COMPLETE, 
						commandExecutionComplete);
				AsyncCommand(command).addEventListener(AsyncCommand.EXECUTION_CANCELED,
						commandExecutionComplete);
			}
			
			for each (command in commands) {
				command.execute();
			}
		}
		
		/**
		 * Runs through the list of commands in reverse order, verifying that
		 * each is undable before calling undo on the individual command.
		 */
		public function undo():void
		{
			var command:AsyncCommand;
			incompleteCommands = new Array();
			
			for each (command in commands) {
				if(command is IUndoableCommand) {
					IUndoableCommand(command).undo();
					incompleteCommands.push(command);
					command.addEventListener(AsyncCommand.EXECUTION_COMPLETE, 
							commandExecutionComplete);
					command.addEventListener(AsyncCommand.EXECUTION_CANCELED,
							commandExecutionComplete);
				}
			}
		}
		
		/**
		 * @private
		 * When a command completes, remove it from the incomplete commands array.
		 * If the array is empty, our macro is complete and we will dispatch
		 * an execution complete.
		 */
		private function commandExecutionComplete(event:Event):void {
			AsyncCommand(event.target).removeEventListener(AsyncCommand.EXECUTION_COMPLETE, 
					commandExecutionComplete);
			AsyncCommand(event.target).removeEventListener(AsyncCommand.EXECUTION_CANCELED, 
					commandExecutionComplete);
			
			// If any of the sub-commands get canceled on execution OR undo, the command
			// will remain in a canceled state and report back that it was canceled on
			// execution complete.
			if (event.type == AsyncCommand.EXECUTION_CANCELED)
			{
				commandFailure = true;
			}
			
			incompleteCommands.splice(incompleteCommands.indexOf(event.target), 1);
			if (incompleteCommands.length == 0) {
				macroComplete(commandFailure);
			}
		}
		
		/**
		 * Called when all commands are complete.  This gives the extending class an opportunity
		 * to execute any logic before dispatching execution complete.
		 * 
		 * @param commandFailure Whether a command failure was encountered while executing
		 * any of the commands.  This allows the developer to take different actions depending on
		 * the success/failure.  While this could have been accessed using a protected class
		 * variable, by making it a parameter it forces the developer to realize that a command
		 * failure may exist and they may need to handle such a case.
		 */
		protected function macroComplete(commandFailure:Boolean):void
		{
			dispatchExecutionComplete(commandFailure);
		}
		
		public function combineCommand(command:ICombinableCommand):void
		{
			commands.push(command);
		}

	}
}